package main

import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/json-iterator/go"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"sync"
)

var (
	mainPageId  = 19383749 //https://www.ximalaya.com/youshengshu/19383749/
	downloadDir = "download"
)

func main() {
	log.SetFlags(log.Ltime | log.Lshortfile)

	//mainPageId:有声小说主页Id, begin: 起始音频, end: 结束音频, maxRoutineCount: 最大协程(建议1-5 太大可能导致磁盘写入跟不上), downloadDir: 下载目录
	err := Start(mainPageId, 1, -1, 5, downloadDir)
	if err != nil {
		log.Fatal(err)
	}
}

func Start(mainPageId, begin, end, maxRoutineCount int, downloadDir string) error {
	//获取有声小说信息
	title, audioCount, _, err := GetAudioBookInfo(mainPageId)
	if err != nil {
		return err
	}
	downloadDir += "/" + title

	ch := make(chan int, maxRoutineCount)
	var wg sync.WaitGroup

	//获取所有音频
	list := GetAudioInfoList(mainPageId, audioCount)
	for i, v := range list {
		i++
		if i >= begin && (i <= end || end == -1) {
			ch <- 1
			wg.Add(1)

			go func(url, dir, fileName string, index int, ch chan int) {
				log.Println("download:", index, fileName, url)

				//去除非法字符
				reg, _ := regexp.Compile("[\\\\/:*?\"<>|]")
				fileName = reg.ReplaceAllString(fileName, "")

				err = download(url, fmt.Sprintf("%s/%s.m4a", dir, fileName), true, ch)
				if err != nil {
					log.Fatal(err)
				}
				wg.Add(-1)
			}(v.Url, downloadDir, v.Title, i, ch)
		}
	}

	wg.Wait()
	return nil
}

type AudioItem struct {
	Id         int
	Title, Url string
}

func GetAudioInfo(mainPageId, page, pageSize int) (audioList []AudioItem, err error) {
	format := fmt.Sprintf("https://m.ximalaya.com/m-revision/common/album/queryAlbumTrackRecordsByPage?albumId=%d&page=%d&pageSize=%d&asc=true", mainPageId, page, pageSize)
	log.Println("Get:", format)

	resp, err := http.Get(format)
	if err != nil {
		return nil, fmt.Errorf("http get %v fail:%v", format, err.Error())
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	resp.Body.Close()

	list := jsoniter.Get(body, "data").Get("trackDetailInfos")
	for i2 := 0; i2 < list.Size(); i2++ {
		v := list.Get(i2).Get("trackInfo")
		audioList = append(audioList, AudioItem{Id: v.Get("id").ToInt(), Url: v.Get("playPath").ToString(), Title: v.Get("title").ToString()})
	}

	return audioList, nil
}

func GetAudioInfoList(mainPageId, audioCount int) (audioList []AudioItem) {
	if audioCount > 100 {
		//分number次Get
		number := audioCount / 100
		for i := 0; i < number; i++ {
			list, err := GetAudioInfo(mainPageId, i+1, 100)
			if err != nil {
				log.Fatal(err)
			}
			audioList = append(audioList, list...)
		}

		j := audioCount % 100
		if j > 0 { //有余数
			list, err := GetAudioInfo(mainPageId, number+1, j)
			if err != nil {
				log.Fatal(err)
			}
			audioList = append(audioList, list...)
		}
	} else { //音频数量 <= 100
		list, err := GetAudioInfo(mainPageId, 1, audioCount)
		if err != nil {
			log.Fatal(err)
		}
		audioList = append(audioList, list...)
	}

	return audioList
}

func GetAudioBookInfo(mainPageId int) (title string, audioCount, pageCount int, err error) {
	resp, err := http.Get(fmt.Sprintf("https://www.ximalaya.com/youshengshu/%d/", mainPageId))
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Fatal("get page fail : StatusCode != 200")
	}

	doc, err := goquery.NewDocumentFromReader(resp.Body)
	if err != nil {
		return "", -1, -1, fmt.Errorf("new goquery document fail:" + err.Error())
	}

	//标题
	title = doc.Find("h1.title").Text()

	//音频数量
	r, _ := regexp.Compile("\\d+\\.?\\d*")
	num := r.FindString(doc.Find("div.head").Text())
	audioCount, _ = strconv.Atoi(num)

	//页面数量
	list := doc.Find("ul.pagination-page").Children()
	size := list.Size()
	if size > 6 { //超过5页
		i, err := strconv.Atoi(list.Eq(list.Size() - 2).Text())
		if err != nil {
			log.Fatal(err)
		}
		pageCount = i
	} else if size == 0 { //就一页
		pageCount = 1
	} else { //大于0页 && 小于等于5页
		pageCount = size - 1 //1为下一页按钮
	}
	return
}

func download(url, filePath string, forceSave bool, ch chan int) error {
	if !forceSave {
		if _, err := os.Stat(filePath); err == nil {
			log.Println("file path exists:", filePath)
			return nil
		}
	}

	resp, err := http.Get(url)
	if err != nil {
		log.Printf("download %s fail: %s", url, err.Error())
		return download(url, filePath, forceSave, ch)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Fatal("download" + url + "fail: StatusCode != 200")
	}

	//目录不存在则创建
	err = os.MkdirAll(filepath.Dir(filePath), 0777)
	if err != nil && !os.IsExist(err) {
		return fmt.Errorf("make dir fail: %s", err.Error())
	}

	//创建并写入文件
	file, err := os.Create(filePath)
	if err != nil {
		return fmt.Errorf("create file %s fail: %s", filePath, err.Error())
	}
	defer file.Close()

	_, err = io.Copy(file, resp.Body)
	if err != nil {
		return fmt.Errorf("io copy %s fail: %s", filePath, err.Error())
	}

	<-ch
	return nil
}
